package comp9417;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by huy on 20/05/14.
 */
public class Classifier {

    private final Dataset dataset;
    private final int attributeIndex;
    private final NaiveNearestNeighbour naiveNN;

    public Classifier(Dataset dataSet, String classifierAttribute) {
        this.dataset = dataSet;
        naiveNN = new NaiveNearestNeighbour(dataset, classifierAttribute);
        attributeIndex = dataset.getAttributeIndex(classifierAttribute);
    }

    /**
     * It is used the the classifier comp9417.Attribute is a number
     *
     * @param point
     * @param k
     * @return answer, I am not sure about the type should be double
     */
    public double predictNumber(DataPoint point, int k) {
        double answer = 0.0;

        List<DataPoint> cPList = naiveNN.nearest(k, point);
        for (DataPoint cp : cPList) {
            double classification = (Double) cp.getValues()[attributeIndex];
            if (Double.isNaN(classification)) {
                k--;
                continue;
            }
            answer += classification;
        }
        answer = answer / k;
        return answer;
    }


    public double predictNumberWeighted(DataPoint point, int k) {
        double answer = 0.0;
        double tempWeight;
        double totalWeight = 0.0;

        LinkedHashMap<Double, DataPoint> nearest = naiveNN.nearestWithDistances(k, point);

        for (Map.Entry<Double, DataPoint> entry : nearest.entrySet()) {
            double classification = (Double) entry.getValue().getValues()[attributeIndex];
            if (Double.isNaN(classification)) continue;
            tempWeight = 1 / entry.getKey(); // weighted distance rule
            totalWeight += tempWeight;
            // just check, might use the same number.
            answer += classification * tempWeight;
        }

        answer = answer / totalWeight;
        return answer;
    }

    public String classifyWeighted(DataPoint point, int k) {
        LinkedHashMap<Double, DataPoint> nearest = naiveNN.nearestWithDistances(k, point);
        double minDistance = Double.MAX_VALUE;
        double maxDistance = Double.MIN_VALUE;
        for (Double distance : nearest.keySet()) {
            if (distance > maxDistance) maxDistance = distance;
            if (distance < minDistance) minDistance = distance;
        }

        if (maxDistance == minDistance) return (String) nearest.values().iterator().next().getValues()[attributeIndex];

        HashMap<String, Double> categories = new HashMap<String, Double>();
        for (Map.Entry<Double, DataPoint> entry : nearest.entrySet()) {
            double weight = (1 - (entry.getKey() - minDistance) / (maxDistance - minDistance));
            String classification = (String) entry.getValue().getValues()[attributeIndex];
            if (categories.containsKey(classification)) {
                categories.put(classification, categories.get(classification) + weight);
            } else {
                categories.put(classification, weight);
            }
        }

        String classification = null;
        double maxValue = Double.MIN_VALUE;
        for (Map.Entry<String, Double> entry : categories.entrySet()) {
            if (entry.getValue() >= maxValue) {
                classification = entry.getKey();
            }
        }
        return classification;
    }

    /**
     * Classify the instance for the group with categorised value
     *
     * @param point
     * @param k
     * @return
     */
    public String classify(DataPoint point, int k) {
        List<DataPoint> cPList = naiveNN.nearest(k, point);

        // not sure if it's a good idea 
        // to count the repetition of one element
        // counting the repetition

        // it string is the category
        // integer part is the repetition
        HashMap<String, Integer> category = new HashMap<String, Integer>();
        for (DataPoint cp : cPList) {
            String classification = (String) cp.getValues()[attributeIndex];
            // just check, might use the same number.
            if (category.containsKey(classification)) {
                category.put(classification, category.get(classification) + 1);
            } else {
                // seen the validation for the first time
                category.put(classification, 1);
            }
        }

        // Doesn't check for the even number and same repetition
        // don't like the implementation
        String maxKey = null; // the key value which repeated the most
        int maxValue = 0;
        for (Map.Entry<String, Integer> entry : category.entrySet()) {
            if (maxValue <= entry.getValue()) {
                maxKey = entry.getKey();
            }
        }
        return maxKey;
    }
}
